Skip to content

fix: normalize legacy task records on load; guard widget render#39

Merged
tintinweb merged 2 commits into
masterfrom
fix/normalize-legacy-tasks
Jul 22, 2026
Merged

fix: normalize legacy task records on load; guard widget render#39
tintinweb merged 2 commits into
masterfrom
fix/normalize-legacy-tasks

Conversation

@tintinweb

Copy link
Copy Markdown
Owner

Problem

Fixes #33. Task files written before the blocking feature have no blockedBy / blocks / metadata. TaskStore.load() deserialized them as-is, and consumers read those fields unguarded (e.g. task.blockedBy.length in the widget). The first widget render on such a task threw TypeError: Cannot read properties of undefined (reading 'length'). Because the widget renders on a pi-tui timer, the throw was uncaught and killed the whole pi process — not just the extension — on the first render after upgrade. Unrecoverable without hand-editing the file.

Fix

Two layers:

  1. Normalize at the load boundary (TaskStore.loadnormalizeTask). Defaults metadata: {}, blocks: [], blockedBy: [] (with type guards for hand-edited files) plus defensive timestamp defaults. This is the correct single fix point: the same unguarded accesses exist across the store's mutation paths and index.ts (including ...task.metadata spreads that ?. can't fix), not only the widget — normalizing once means every consumer receives a canonical Task.
  2. Guard the widget renderrenderWidget now wraps the build in try/catch and returns a safe fallback, so a render error can never again escape to the TUI timer and take down the host. Defense-in-depth against the whole class of "extension render throw crashes pi".

Tests

  • test/task-store.test.ts — loading a legacy-shaped file (pending task, no blockedBy/blocks/metadata) yields a task with those fields defaulted, existing fields preserved.
  • test/task-widget.test.ts — rendering a task missing legacy fields does not throw.
  • Verified the exact issue scenario end-to-end (legacy file → load → widget renders the subject, no crash).

Credit

Normalization approach based on #30 by @arrokh (thanks!) — this supersedes it. Reported by @eSaadster.

Test plan

  • npx tsc --noEmit — clean
  • npx vitest run — 190 passing (incl. 2 new)
  • npx biome check src/ test/ — clean

tintinweb and others added 2 commits July 22, 2026 18:26
Task files written before the blocking feature have no blockedBy/blocks/
metadata. TaskStore.load() deserialized them as-is, so consumers reading
those fields unguarded (e.g. task.blockedBy.length in the widget) threw a
TypeError. Because the widget renders on a pi-tui timer, the throw was
uncaught and killed the whole pi process on the first render after upgrade.

- Normalize every record at the load() boundary (normalizeTask): default
  metadata/blocks/blockedBy, with type guards for hand-edited files. This is
  the single fix point — the same unguarded accesses exist across the store
  and index mutation paths, not just the widget.
- Wrap the widget render in try/catch returning a safe fallback, so a render
  error can never again escape to the TUI timer and crash the host.

Fixes #33. Normalization approach based on #30 (thanks @arrokh); reported by
@eSaadster.

Co-authored-by: Noor Octavian (Alvin) Anwar <nooroctaviananwar@gmail.com>
…y-tasks

# Conflicts:
#	test/task-store.test.ts
@tintinweb
tintinweb merged commit 9e0eb3b into master Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant